#include <Bitmap.hpp>
#include <Camera.hpp>
#include <Material.hpp>
#include <Ray.hpp>
#include <Sphere.hpp>
#include <glm/gtc/constants.hpp>
#include <glm/gtc/random.hpp>
#include <iostream>

static float random_float(float min = 0.f, float max = 1.f) {
  return glm::linearRand(min, max);
}

static glm::vec3 random_vec3(float min = 0.f, float max = 1.f) {
  return glm::linearRand(glm::vec3{min, min, min}, glm::vec3{max, max, max});
}

static std::shared_ptr<HittableList> random_scene() {
  std::shared_ptr<HittableList> world = std::make_shared<HittableList>();

  std::shared_ptr<Material> ground_material =
      std::make_shared<Lambertian>(glm::vec3{0.5f, 0.5f, 0.5f});

  world->AddObject(std::make_shared<Sphere>(glm::vec3{0.f, -1000.f, 0.f},
                                            1000.f, ground_material));

  for (int32_t a = -11; a < 11; a++) {
    for (int32_t b = -11; b < 11; b++) {
      float choose_mat = random_float();
      glm::vec3 center{a + 0.9f * random_float(), 0.2f,
                       b + 0.9f * random_float()};

      if (glm::length(center - glm::vec3{4.f, 0.2f, 0}) > 0.9f) {
        std::shared_ptr<Material> sphere_material;

        if (choose_mat < 0.8) {
          // diffuse
          sphere_material =
              std::make_shared<Lambertian>(random_vec3() * random_vec3());
        } else if (choose_mat < 0.95f) {
          // metal
          auto albedo = random_vec3(0.5f, 1.f);
          auto fuzz = random_float(0.f, 0.5f);
          sphere_material = std::make_shared<Metal>(albedo, fuzz);
        } else {
          // glass
          sphere_material = std::make_shared<Dielectric>(1.5f);
        }

        world->AddObject(
            std::make_shared<Sphere>(center, 0.2f, sphere_material));
      }
    }
  }

  auto material1 = std::make_shared<Dielectric>(1.5f);
  world->AddObject(
      std::make_shared<Sphere>(glm::vec3{0.f, 1.f, 0.f}, 1.f, material1));

  auto material2 = std::make_shared<Lambertian>(glm::vec3{0.4f, 0.2f, 0.1f});
  world->AddObject(
      std::make_shared<Sphere>(glm::vec3{-4.f, 1.f, 0.f}, 1.f, material2));

  auto material3 = std::make_shared<Metal>(glm::vec3{0.7f, 0.6f, 0.5f});
  world->AddObject(
      std::make_shared<Sphere>(glm::vec3{4.f, 1.f, 0.f}, 1.f, material3));

  return world;
}

static glm::vec3 ray_color(const Ray& ray, Hittable* world, int32_t depth) {
  HitRecord rec;

  if (depth <= 0) {
    return glm::vec3{};
  }

  if (world->Hit(ray, 0.001f, infinity, rec)) {
    if (!rec.material) {
      return glm::vec3{};
    }
    Ray scattered;
    glm::vec3 attenuation;
    if (rec.material->Scatter(ray, rec, attenuation, scattered)) {
      return attenuation * ray_color(scattered, world, depth - 1);
    }

    return glm::vec3{};
  }

  glm::vec3 unit_direction = glm::normalize(ray.Direction());
  float t = 0.5f * (unit_direction.y + 1.f);
  return (1.f - t) * glm::vec3(1.f, 1.f, 1.f) + t * glm::vec3(0.5f, 0.7f, 1.f);
}

int main(int argc, const char** argv) {
  float aspect_ratio = 3.f / 2.f;
  int32_t image_width = 1200;
  // image
  auto bitmap = Bitmap::CreateBitmap(image_width, aspect_ratio);

  int32_t samples_per_pixel = 500;
  int32_t max_depth = 50;

  // world
  auto world = random_scene();

  // camera
  glm::vec3 look_from{13.f, 2.f, 3.f};
  glm::vec3 look_at{0.f, 0.f, 0.f};
  glm::vec3 vup{0.f, 1.f, 0.f};
  float dist_to_focus = 10.f;
  float aperture = 0.1f;
  Camera camera(look_from, look_at, vup, 20, aspect_ratio, aperture, dist_to_focus);

  for (uint32_t i = 0; i < bitmap->Width(); i++) {
    std::cout << "render line: " << i << std::endl;
    for (uint32_t j = 0; j < bitmap->Height(); j++) {
      glm::vec3 pixel_color{};
      for (int32_t s = 0; s < samples_per_pixel; s++) {
        auto u = float(i + glm::linearRand(0.f, 1.f)) / (bitmap->Width() - 1);
        auto v = float(j + glm::linearRand(0.f, 1.f)) / (bitmap->Height() - 1);
        Ray ray = camera.GetRay(u, v);

        pixel_color += ray_color(ray, world.get(), max_depth);
      }

      bitmap->setPixel(i, bitmap->Height() - 1 - j, pixel_color,
                       samples_per_pixel);
    }
  }

  bitmap->writeToFile("final_render.png");

  return 0;
}